package xj.graph2d.shapes;

import static xj.graph2d.DrawShapeConstants.PointOutside;
import static xj.graph2d.DrawShapeConstants.d;
import static xj.util.xml.XMLUtil.getXMLAttributeString;
import static xj.graph2d.graph.Geometry.angle;
import static xj.graph2d.graph.Geometry.distance;

import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import xj.graph2d.DrawShapeBase;
import xj.graph2d.GraphEdgeBase;
import xj.graph2d.GraphNodeBase;
import xj.lang.CodeWriter;
import xj.util.gui.Point2DFloat;

public abstract class CurveShapeBase 
  extends GraphEdgeBase 
  implements CurveShape {

  protected CurveShapeBase() {
    this(false); // no graph componnet
    allowRotation = false;
  }

  protected CurveShapeBase(boolean createComp) {
    super(createComp);
    // ctr1 = new Point2DFloat();
    // ctr2 = new Point2DFloat();
    allowRotation = false;
  }

  @Override
  public void copyTo(DrawShapeBase s) 
    throws CloneNotSupportedException {
    if (s != null) {
      super.copyTo(s);
      if (s instanceof CurveShapeBase) {
	CurveShapeBase curve = (CurveShapeBase) s;
	if (ctr1 != null) {
	  curve.setCtr1((float) ctr1.getX(), (float) ctr1.getY());
	} else {
	  curve.ctr1 = null;
	}
	if (ctr2 != null) {
	  curve.setCtr2((float) ctr2.getX(), (float) ctr2.getY());
	} else {
	  curve.ctr2 = null;
	}
      }
    }
  }

  @Override
  public String getName() {
    return "Curve";
  }

  @Override
  public void normalize() {
  }

  public Point2D getCtr1() {
    return ctr1;
  }

  public Point2D getCtr2() {
    return ctr2;
  }

  public void setCtr1(float x, float y) {
    if (ctr1 == null) {
      ctr1 = new Point2DFloat(x, y);
    } else {
      ctr1.setLocation(x, y);
    }
  }

  public void setCtr2(float x, float y) {
    if (ctr2 == null) {
      ctr2 = new Point2DFloat(x, y);
    } else {
      ctr2.setLocation(x, y);
    }
  }

  public void setPoints(float x1, float y1, float x2, float y2) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
  }

  public void setPoints(float x1, float y1, float x2, float y2, float cx1,
      float cy1) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    setCtr1(cx1, cy1);

  }

  public void setPoints(float x1, float y1, float x2, float y2, float cx1,
      float cy1, float cx2, float cy2) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    setCtr1(cx1, cy1);
    setCtr2(cx2, cy2);
  }

  @Override
  public void moveEnd1(float dx, float dy) {
    p1.setLocation(p1.getX() + dx, p1.getY() + dy);
    curve = null;
    calculateBounds();
  }

  @Override
  public void moveEnd2(float dx, float dy) {
    p2.setLocation(p2.getX() + dx, p2.getY() + dy);
    curve = null;
    calculateBounds();
  }

  public void moveEnd1Adj(float dx, float dy) {
    p1.setLocation(p1.getX() + dx, p1.getY() + dy);
    adjustControlPoints();
    curve = null;
    calculateBounds();
  }

  public void moveEnd2Adj(float dx, float dy) {
    p2.setLocation(p2.getX() + dx, p2.getY() + dy);
    adjustControlPoints();
    curve = null;
    calculateBounds();
  }

  public void moveCtr1(float dx, float dy) {
    if (ctr1 != null) {
      ctr1.setLocation(ctr1.getX() + dx, ctr1.getY() + dy);
    } else {
      ctr1 = new Point2DFloat(dx, dy);
    }
    curve = null;
    calculateBounds();
  }

  public void moveCtr2(float dx, float dy) {
    if (ctr2 != null) {
      ctr2.setLocation(ctr2.getX() + dx, ctr2.getY() + dy);
    } else {
      ctr2 = new Point2DFloat(dx, dy);
    }
    curve = null;
    calculateBounds();
  }

  @Override
  public void move(float dx, float dy) {
    p1.setLocation(p1.getX() + dx, p1.getY() + dy);
    p2.setLocation(p2.getX() + dx, p2.getY() + dy);
    if (ctr1 != null) {
      ctr1.setLocation(ctr1.getX() + dx, ctr1.getY() + dy);
    }
    if (ctr2 != null) {
      ctr2.setLocation(ctr2.getX() + dx, ctr2.getY() + dy);
    }
    curve = null;
    calculateBounds();
  }

  public void setEndsAdj(float x1, float y1, float x2, float y2,
			 boolean scaleCtrl) {
    if (scaleCtrl) {
      //scaleControlPoints(x1, x2, y1, y2);
      scaleControlPointsLinear(x1, x2, y1, y2);
    }
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    curve = null;
    calculateBounds();
  }

  public boolean isFlippable() {
    return true;
  } 

  public void flipOrientation() {
    flipControlPoints();
  }

  public void flipControlPoints() { 
    if (ctr1 != null) {
      double x1 = p1.getX();
      double y1 = p1.getY();
      double a1 = angle(p1, p2); 
      double a2 = angle(p1, ctr1);       
      double a3 = a1 - (a2 - a1);
      double d = distance(p1, ctr1); 
      double cx1 = x1 + d * Math.cos(a3);
      double cy1 = y1 + d * Math.sin(a3);
      ctr1.setLocation(cx1, cy1);
    }
    if (ctr2 != null) {
      double x2 = p2.getX();
      double y2 = p2.getY();
      double a1 = angle(p2, p1); 
      double a2 = angle(p2, ctr2);       
      double a3 = a1 - (a2 - a1);
      double d = distance(p2, ctr2); 
      double cx2 = x2 + d * Math.cos(a3);
      double cy2 = y2 + d * Math.sin(a3);
      ctr2.setLocation(cx2, cy2);
    }
  }

  public void scaleControlPoints(float x1, float y1, float x2, float y2) {

  }

  public void scaleControlPointsLinear(float x1, float y1, float x2, float y2) {
    float x = (float) p1.getX();
    float y = (float) p1.getY();
    float w = (float) bounds.getWidth();
    float h = (float) bounds.getHeight();

    float sx = 1;
    float sy = 1;
    if (x1 == x2) {
      sx = 0;
    } else if (w != 0) {
      sx = Math.abs(x2 - x1) / w;
    }
    if (y1 == y2) {
      sy = 0;
    } else if (h != 0) {
      sy = Math.abs(y2 - y1) / h;
    }

    float px, py;
    if (ctr1 != null) {
      px = (float) ctr1.getX();
      py = (float) ctr1.getY();
      px = x1 + (px - x) * sx;
      py = y1 + (py - y) * sy;
      ctr1.setLocation(px, py);
    }
    if (ctr2 != null) {
      px = (float) ctr2.getX();
      py = (float) ctr2.getY();
      px = x1 + (px - x) * sx;
      py = y1 + (py - y) * sy;
      ctr2.setLocation(px, py);
    }
  }

  @Override
  public void setEndsAdj(float x1, float y1, float x2, float y2) {
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    adjustControlPoints();
    curve = null;
    calculateBounds();
  }

  @Override
  public void scaleEnds(float x1, float y1, float x2, float y2) {
    float xmin = Math.min(x1, x2);
    float ymin = Math.min(y1, y2);
    float x = (float) bounds.getX();
    float y = (float) bounds.getY();
    float w = (float) bounds.getWidth();
    float h = (float) bounds.getHeight();
    float sx = Math.abs(x2 - x1) / w;
    float sy = Math.abs(y2 - y1) / h;
    scale(sx, sy);
    move(xmin - x, ymin - y);
  }

  @Override
  public void scale(float sx, float sy) {
    float x0 = (float) bounds.getX();
    float y0 = (float) bounds.getY();
    float x1 = (float) p1.getX();
    float y1 = (float) p1.getY();
    float x2 = (float) p2.getX();
    float y2 = (float) p2.getY();
    x1 = x0 + (x1 - x0) * sx;
    y1 = y0 + (y1 - y0) * sy;
    x2 = x0 + (x2 - x0) * sx;
    y2 = y0 + (y2 - y0) * sy;
    p1.setLocation(x1, y1);
    p2.setLocation(x2, y2);
    if (ctr1 != null) {
      float cx1 = (float) ctr1.getX();
      float cy1 = (float) ctr1.getY();
      cx1 = x0 + (cx1 - x0) * sx;
      cy1 = y0 + (cy1 - y0) * sy;
      ctr1.setLocation(cx1, cy1);
    }
    if (ctr2 != null) {
      float cx2 = (float) ctr2.getX();
      float cy2 = (float) ctr2.getY();
      cx2 = x0 + (cx2 - x0) * sx;
      cy2 = y0 + (cy2 - y0) * sy;
      ctr2.setLocation(cx2, cy2);
    }
  }

  @Override
  public int isInside(Point2D p) {
    if (p != null) {
      float px = (float) p.getX();
      float py = (float) p.getY();
      return isInside(px, py);
    } else {
      return PointOutside;
    }
  }

  @Override
  public float getShapeWidth() {
    return (float) bounds.getWidth();
  }

  @Override
  public float getShapeHeight() {
    return (float) bounds.getHeight();
  }

  @Override
  public Shape getOutlineShape() {
    return curve;
  }

  @Override
  public void calculateBounds() {
    if (curve == null) {
      makeCurve();
    }
    bounds.setRect(curve.getBounds());
    if (subshapes != null && subshapes.size() > 0) {
      Rectangle2D sub = getSubshapeBounds();
      if (sub != null) {
	Rectangle2D.union(bounds, sub, bounds);
      }
    }

    framebounds.setRect(bounds.getX() - d, bounds.getY() - d, 
			bounds.getWidth() + 2 * d, bounds.getHeight() + 2 * d);
    adjustBounds();
  }

  @Override
  public void adjustEnds(GraphNodeBase src, 
			 GraphNodeBase des, 
			 boolean adjustCtrPts) {
    super.adjustEnds(src, des, adjustCtrPts);
    curve = null;
    calculateBounds();
  }

  @Override
  protected void writeXMLElements(CodeWriter out, int mode,
				  String namespacePrefix) {
    if (out != null) {
      super.writeXMLElements(out, mode, namespacePrefix);

      String nodename;
      if (ctr1 != null) {
	nodename = getPrefixedName("CurveCtr1", namespacePrefix);
	out.println("<" + nodename + ' ' + 
		    getXMLAttributeString("x", ctr1.getX()) + ' ' + 
		    getXMLAttributeString("y", ctr1.getY()) + "/>");
      }

      if (ctr2 != null) {
	nodename = getPrefixedName("CurveCtr2", namespacePrefix);
	out.println("<" + nodename + ' ' + 
		    getXMLAttributeString("x", ctr2.getX()) + ' ' + 
		    getXMLAttributeString("y", ctr2.getY()) + "/>");
      }
    }
  }

  @Override
  protected void processXMLElement(org.w3c.dom.Element e, String namespaceURI) {
    if (e != null) {
      String cname = e.getLocalName();
      if ("CurveCtr1".equals(cname) || "CurveCtr2".equals(cname)) {
	String xs, ys;
	float x, y;
	xs = e.getAttribute("x");
	ys = e.getAttribute("y");
	try {
	  x = Float.parseFloat(xs);
	  y = Float.parseFloat(ys);
	  if ("CurveCtr1".equals(cname)) {
	    setCtr1(x, y);
	  } else {
	    setCtr2(x, y);
	  }
	} catch (NumberFormatException ex) {
	}
	curve = null;
      } else {
	super.processXMLElement(e, namespaceURI);
      }
    }
  }

  protected boolean isOnCurveBoundary(float x, float y, float d) { 
    if (curve != null) { 
      if (curve.contains(x, y)) { 
	if (!curve.contains(x - d, y - d) || 
	    !curve.contains(x - d, y + d) || 
	    !curve.contains(x + d, y - d) || 
	    !curve.contains(x + d, y + d)) { 
	  Line2D line = new Line2D.Float(p1, p2);
	  double dist = line.ptLineDist(x, y);	  
	  if (dist <= 1.5 * d) { 
	    if (ctr2 == null) { 
	      Line2D line1 = new Line2D.Float(p1, ctr1);
	      Line2D line2 = new Line2D.Float(p2, ctr1);
	      double d1 = line1.ptLineDist(x, y);	  
	      double d2 = line2.ptLineDist(x, y);	  
	      if (d1 > 2 * d && 
		  d2 > 2 * d) {
		return false; 
	      }
	    } else { 
	      Line2D line1 = new Line2D.Float(p1, ctr1);
	      Line2D line2 = new Line2D.Float(p2, ctr2);
	      double d1 = line1.ptLineDist(x, y);	  
	      double d2 = line2.ptLineDist(x, y);	  
	      if (d1 > 2 * d && 
		  d2 > 2 * d) {
		return false; 
	      }
	    }
	  }
	  return true; 
	}
      }
    }
    return false; 
  }

  abstract protected void makeCurve();

  protected Point2D ctr1, ctr2;

  protected Shape curve;

}
